/**
 * 
 */
package profile.aspect;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.omg.CORBA.portable.InputStream;
import org.omg.CORBA.portable.OutputStream;

import profile.ServerInfo;
import profile.ServerInfoHolder;


/**
 * @author ductrung.tran
 *
 */
@Aspect
public abstract class ClientProfilingAspect {
	
	private MethodCallRepository resultHandler;
	
	{
		resultHandler = new MethodCallRepository(getFileName());
		MethodCallRepository.setInstance(resultHandler);
		//System.out.println("init " + ServerInfoHolder.getServerInfo());
	}

	private String getFileName() {
		String outputDir = getOutputDir();
		ServerInfo info = ServerInfoHolder.getServerInfo();
		if (info == null)
			return outputDir + "/ProfilingResult-" + System.currentTimeMillis() + ".txt";
		
		return outputDir + "/ProfilingResult-" + info.getVersion() + "-" + info.getServer() + "-" + info.getDatabase() + "-" + System.currentTimeMillis() + ".txt";
	}
	
	private String getOutputDir() {
		return System.getProperty("perf.output", "./target/performance");
	}

	@Pointcut
	public abstract void eliotStoreStubInvokePointcut();
	
	@Around("eliotStoreStubInvokePointcut()")
	public Object eliotStoreStubInvokeAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
		//System.out.println("advice " + ServerInfoHolder.getServerInfo());
		Object arg = joinPoint.getArgs()[0];
		OutputStream output = (OutputStream) arg;
		output.write_longlong(MethodCall.getId());
		long start = 0;
		try {
			start = System.currentTimeMillis();
			return joinPoint.proceed();
		} finally {
			MethodCall.get().setTime("corbaTime", System.currentTimeMillis() - start);
		}
	}

	@Pointcut
	public abstract void eliotStorePointcut();
	
	@Around("eliotStorePointcut()")
	public Object eliotStoreAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = 0;
		MethodCall ctx = null;
		try {
			ctx = MethodCall.get();
			ctx.setName(buildName(joinPoint));
			start = System.currentTimeMillis();
			return joinPoint.proceed();
		} finally {
			ctx.setExecTime(System.currentTimeMillis() - start); 
			addProfilingResult(ctx);
			MethodCall.remove();
			//Thread.dumpStack();
		}
	}

	private void addProfilingResult(MethodCall ctx) {
		resultHandler.addProfilingResult(ctx);
	}

	private String buildName(ProceedingJoinPoint joinPoint) {
		StringBuilder builder = new StringBuilder();
		
		Class<?>[] interfaces = joinPoint.getThis().getClass().getInterfaces();
		for (int i = 0; i < interfaces.length; i++) {
			String simpleName = interfaces[i].getSimpleName();
			if (simpleName.matches("^Eliot.*Store$") || simpleName.equals("EliotPosMgr")) {
				builder.append(interfaces[i].getSimpleName()).append('.');
				break;
			}
		}
		
		builder.append(joinPoint.getSignature().getName()).append('(');
		
		MethodSignature signature =(MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		if (joinPoint.getArgs().length > 1) {
			// ignore EDSConnection in args
			for (int i = 1; i < joinPoint.getArgs().length; i++) {
				Object arg = joinPoint.getArgs()[i];
				builder.append(method.getParameterTypes()[i].getSimpleName()).append(':').append(toString(arg)).append(',');
			}
			
			builder.deleteCharAt(builder.length()-1);
		}
		builder.append(')');
		
		return builder.toString().replaceAll("\n", "");
	}
	
	private Object toString(Object arg) {
		if (arg == null) {
			return null;
		}
		
		if (arg.getClass().getName().equals("jeliot.util.BasicDate")) {
			return getField("date", arg);
		}
		if (arg.getClass().getName().equals("jeliot.util.BasicTime")) {
			return getField("timestamp", arg);
		}
		if (arg.getClass().getSimpleName().matches("^Eliot.*[Data|DataImpl]$")) {
			return toStringData(arg);
		}
		if (arg.getClass().isArray()) {
			return toArrayData(arg);
		}
		
		
		return arg instanceof String ? arg.toString().replaceAll(",", "\\,") : arg;
	}

	private Object toArrayData(Object arg) {
		StringBuilder builder = new StringBuilder();
		builder.append('[');
		int length = Array.getLength(arg);
		if (length > 0) {
			for (int i = 0; i < length; i++) {
				builder.append(toString(Array.get(arg, i))).append(',');
			}
			builder.deleteCharAt(builder.length()-1);
		}
		builder.append(']');
		return builder.toString();
	}

	private Object toStringData(Object arg) {
		StringBuilder builder = new StringBuilder();
		builder.append(arg.getClass().getSimpleName()).append('{');
		for (Field field : arg.getClass().getFields()) {
			builder.append(field.getName()).append(':').append(toString(getField(field, arg))).append(',');
		}
		builder.deleteCharAt(builder.length()-1);
		builder.append('}');
		return builder.toString();
	}

	private Object getField(Field field, Object arg) {
		try {
			return field.get(arg);
		} catch (Exception e) {
			System.err.println("Cannot get value from field " + field.getName() + " on " +arg);
			return null;
		}
	}
	
	private Object getField(String string, Object arg) {
		try {
			return getField(arg.getClass().getField(string), arg);
		} catch (Exception e) {
			System.err.println("Cannot get field " + string + " on " +arg);
			return null;
		}
	}

	@Pointcut
	public abstract void releaseReplyPointcut();
	
	@Before("releaseReplyPointcut()")
	public void releaseReplyAdvice(JoinPoint joinPoint) throws Throwable {
		InputStream input = (InputStream) joinPoint.getArgs()[0];
		try {
			MethodCall.get().setTime("serverTime", input.read_longlong());
		} catch (Throwable t) {
			MethodCall.get().setTime("serverTime", -1);
		}
	}
}
